Recalls NHTSA

O dataset utilizado apresenta ocorrências de recalls em veículos, disponibilizada pela National Highway Traffic Safety Administration (NHTSA).

Recall é um termo usado em diversos contextos, mas geralmente se refere a uma ação de correção ou retirada de um produto ou serviço que apresentou problemas de segurança ou eficácia. Um recall de veículos é uma ação corretiva realizada pela fabricante de um automóvel devido a problemas identificados com alguma peça ou componente do veículo. Esse tipo de recall é feito para corrigir questões de segurança ou de funcionamento do veículo antes que elas possam causar problemas para os proprietários ou para outros usuários da estrada. Em geral, o proprietário do veículo é notificado pela fabricante sobre o recall e pode levar o veículo para ser consertado em uma concessionária autorizada sem custo algum.

A NHTSA, que em português significa Administração Nacional de Segurança de Tráfego Rodoviário, é uma agência do departamento de transporte dos Estados Unidos responsável por regulamentar e garantir a segurança de veículos e equipamentos relacionados à segurança viária, incluindo regulamentação de normas para veículos, equipamentos de segurança, e a investigação de acidentes de trânsito. A NHTSA também é responsável por monitorar e realizar recalls de veículos quando necessário, a fim de garantir a segurança dos usuários da estrada.

Obtenção, estrutura e limpeza dos dados

Os dados foram obtidos diretamente do site da NHTSA. Na seção de recalls é disponibilizado o arquivo FLAT_RCL.zip que contém um arquivo .txt que foi utilizado como dataset.

Existe uma versão deste arquivo no formato csv e com os dados tratados. Porém, esta fonte de dados não será utilizada, pois não possui informações detalhadas dos modelo dos veículos.

Um dicionário de todos os dados disponibilizados está disponível em: https://static.nhtsa.gov/odi/ffdd/rcl/RCL.txt.

Code
import plotly.express as px
import plotly.graph_objects as go
import plotly.offline as pyo
import seaborn as sns
import matplotlib.pyplot as plt
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd
from datetime import date
from wordcloud import WordCloud
import requests
import seaborn as sns

pyo.init_notebook_mode()

RECALLS_DATASET_COLUMNS = ['RECORD_ID', 'CAMPNO', 'MAKETXT', 'MODELTXT', 'YEARTXT',
                           'MFGCAMPNO', 'COMPNAME', 'MFGNAME', 'BGMAN', 'ENDMAN',
                           'RCLTYPECD', 'POTAFF', 'ODATE', 'INFLUENCED_BY',
                           'MFGTXT', 'RCDATE', 'DATEA', 'RPNO', 'FMVSS','DESC_DEFECT',
                           'CONEQUENCE_DEFECT', 'CORRECTIVE_ACTION', 'NOTES',
                           'RCL_CMPT_ID', 'MFR_COMP_NAME', 'MFR_COMP_DESC', 'MFR_COMP_PTNO']

df = pd.read_csv('data/FLAT_RCL.txt', sep='\t', names=RECALLS_DATASET_COLUMNS, on_bad_lines='skip', low_memory=False)

Após importado, o dataset é apresentado conforme o modelo seguinte:

Code
df.head(5)
RECORD_ID CAMPNO MAKETXT MODELTXT YEARTXT MFGCAMPNO COMPNAME MFGNAME BGMAN ENDMAN ... RPNO FMVSS DESC_DEFECT CONEQUENCE_DEFECT CORRECTIVE_ACTION NOTES RCL_CMPT_ID MFR_COMP_NAME MFR_COMP_DESC MFR_COMP_PTNO
0 1 02V288000 FORD FOCUS 2000 02S41 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES FORD MOTOR COMPANY 19990719.0 20010531.0 ... NaN NaN CERTAIN PASSENGER VEHICLES EQUIPPED WITH ZETEC... THIS, IN TURN, COULD CAUSE THE BATTERY CABLES ... DEALERS WILL INSPECT THE BATTERY CABLES FOR TH... ALSO CONTACT THE NATIONAL HIGHWAY TRAFFIC SAFE... 000015339000215021000000202 NaN NaN NaN
1 2 02V288000 FORD FOCUS 2001 02S41 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES FORD MOTOR COMPANY 19990719.0 20010531.0 ... NaN NaN CERTAIN PASSENGER VEHICLES EQUIPPED WITH ZETEC... THIS, IN TURN, COULD CAUSE THE BATTERY CABLES ... DEALERS WILL INSPECT THE BATTERY CABLES FOR TH... ALSO CONTACT THE NATIONAL HIGHWAY TRAFFIC SAFE... 000015339000215022000000202 NaN NaN NaN
2 3 02V236000 JAYCO FT EAGLE 10 SG 2003 NaN EQUIPMENT:OTHER:LABELS JAYCO, INC. 20020730.0 20020813.0 ... NaN NaN ON CERTAIN FOLDING TENT CAMPERS, THE FEDERAL C... IF THE TIRES WERE INFLATED TO 80 PSI, THEY COU... OWNERS WILL BE MAILED CORRECT LABELS FOR INSTA... ALSO, CUSTOMERS CAN CONTACT THE NATIONAL HIGHW... 000015210000106403000000349 NaN NaN NaN
3 4 02V237000 HOLIDAY RAMBLER ENDEAVOR 2000 NaN STRUCTURE MONACO COACH CORP. NaN NaN ... NaN NaN ON CERTAIN CLASS A MOTOR HOMES, THE FLOOR TRUS... CONDITIONS CAN RESULT IN THE BOTTOMING OUT THE... DEALERS WILL INSPECT THE FLOOR TRUSS NETWORK S... CUSTOMERS CAN ALSO CONTACT THE NATIONAL HIGHWA... 000015211000083965000000272 NaN NaN NaN
4 5 02V237000 HOLIDAY RAMBLER ENDEAVOR 1999 NaN STRUCTURE MONACO COACH CORP. NaN NaN ... NaN NaN ON CERTAIN CLASS A MOTOR HOMES, THE FLOOR TRUS... CONDITIONS CAN RESULT IN THE BOTTOMING OUT THE... DEALERS WILL INSPECT THE FLOOR TRUSS NETWORK S... CUSTOMERS CAN ALSO CONTACT THE NATIONAL HIGHWA... 000015211000080938000000272 NaN NaN NaN

5 rows × 27 columns

Renomeando e selecionando colunas

O dataset possui um grande conjunto de colunas para a descrição dos recalls, porém, essa análise será realizada apenas sobre um subconjunto dos dados do dataset. Features como descrição e correção do recall não foram consideradas nesta análise.

Assim, a análise será focada nas colunas conforme dados e tipos detalhados abaixo:

Variable Type/Size Definitions
CAMPNO CHAR(12) NHTSA CAMPAIGN NUMBER
MAKETXT CHAR(25) VEHICLE/EQUIPMENT MAKE
MODELTXT CHAR(256) VEHICLE/EQUIPMENT MODEL
YEARTXT CHAR(4) MODEL YEAR, 9999 IF UNKNOWN or N/A
COMPNAME CHAR(256) COMPONENT DESCRIPTION
RCLTYPECD CHAR(4) VEHICLE, EQUIPMENT OR TIRE REPORT
POTAFF NUMBER(9) POTENTIAL NUMBER OF UNITS AFFECTED
MFGTXT CHAR(40) MANUFACTURERS OF RECALLED VEHICLES/PRODUCTS
RCDATE CHAR(8) REPORT RECEIVED DATE
CONEQUENCE_DEFECT CHAR(2000) CONSEQUENCE SUMMARY

De forma a facilitar a compreensão e acesso aos dados, as colunas selecionadas, foram também renomeadas.

Code
RENAMED_COLUMNS = {
    'CAMPNO': 'RECALL_ID',
    'MAKETXT': 'VEHICLE_MAKE',
    'MODELTXT': 'VEHICLE_MODEL',
    'YEARTXT': 'VEHICLE_YEAR',
    'COMPNAME': 'COMPONENT_NAME',
    'RCLTYPECD': 'RECALL_TYPE_CODE',
    'POTAFF': 'AFFECTED_UNITS',
    'MFGTXT': 'MANUFACTURER',
    'RCDATE': 'REPORTED_DATE',
    'CONEQUENCE_DEFECT': 'CONSEQUENCE_SUMMARY'
}

df_raw = df
df.rename(RENAMED_COLUMNS, axis='columns', inplace=True)
Code
REQUIRED_COLUMNS = [
    'RECALL_ID',
    'VEHICLE_MAKE',
    'VEHICLE_MODEL',
    'VEHICLE_YEAR',
    'COMPONENT_NAME',
    'RECALL_TYPE_CODE',
    'AFFECTED_UNITS',
    'MANUFACTURER',
    'REPORTED_DATE'
]

df = df[REQUIRED_COLUMNS]

Após renomeadas e filtradas, o dataset é apresentado como apresentado abaixo:

Code
df.head(5)
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE
0 02V288000 FORD FOCUS 2000 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES V 291854.0 Ford Motor Company 20021106
1 02V288000 FORD FOCUS 2001 ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES V 291854.0 Ford Motor Company 20021106
2 02V236000 JAYCO FT EAGLE 10 SG 2003 EQUIPMENT:OTHER:LABELS V 86.0 Jayco, Inc. 20020904
3 02V237000 HOLIDAY RAMBLER ENDEAVOR 2000 STRUCTURE V 6223.0 MONACO COACH CORPORATION 20020909
4 02V237000 HOLIDAY RAMBLER ENDEAVOR 1999 STRUCTURE V 6223.0 MONACO COACH CORPORATION 20020909

Recall ID

Um recall possui um identificador único, que segue um padrão específico. Assim, a fim de seguir com dados válidos, podemos remover recalls em que o ID não segue o padrão definido pela NHTSA.

Code
VALID_RECALL_ID_PATTERN = '\d{2}[A-Z]{1}\d{2}(\d{1}|[A-Z])\d{3}'

invalid_ids = df[~df['RECALL_ID'].str.match(VALID_RECALL_ID_PATTERN)].index

df = df.drop(invalid_ids, axis=0)

Tipos de Recalls

O dataset categoriza os recalls em alguns grupos, a partir de códigos de tipos de recall (coluna RECALL_TYPE_CODE), que apresenta os seguintes valores ‘V’, ‘E’, ‘T’, ‘X’, ‘C’ e ‘I’.

Code
df['RECALL_TYPE_CODE'].unique()
array(['V', 'E', 'T', 'X', 'C', 'I'], dtype=object)

Recalls do tipo E, T, X e C não estão relacionadas a um recall de uma marca de veículo, mas de uma marca de equipamento (E), pneu (T e X) ou assento para criança (C).

Apenas os recalls do tipo V e I serão analisados, pois ambos os tipos informam o modelo do veículo. Recalls de outros tipos serão desconsiderados.

Code
df[df['RECALL_TYPE_CODE'] == 'V'][['RECALL_ID', 'RECALL_TYPE_CODE', 'VEHICLE_MAKE', 'VEHICLE_MODEL']].head(5)
RECALL_ID RECALL_TYPE_CODE VEHICLE_MAKE VEHICLE_MODEL
0 02V288000 V FORD FOCUS
1 02V288000 V FORD FOCUS
2 02V236000 V JAYCO FT EAGLE 10 SG
3 02V237000 V HOLIDAY RAMBLER ENDEAVOR
4 02V237000 V HOLIDAY RAMBLER ENDEAVOR
Code
df[df['RECALL_TYPE_CODE'] == 'I'][['RECALL_ID', 'RECALL_TYPE_CODE', 'VEHICLE_MAKE', 'VEHICLE_MODEL']].head(5)
RECALL_ID RECALL_TYPE_CODE VEHICLE_MAKE VEHICLE_MODEL
898 02I001000 I NISSAN ALTIMA
936 02I002000 I ISUZU AMIGO
937 02I002000 I ISUZU RODEO
938 02I002000 I ISUZU AMIGO
939 02I002000 I ISUZU RODEO

Mesmo após filtrados, os recalls nessas categorias de componente ainda representam cerca de 90% das entradas originais do dataset.

Code
percentual_recalls_veiculos = df[df['RECALL_TYPE_CODE'].isin(['V', 'I'])]['RECALL_TYPE_CODE'].count() / df['RECALL_TYPE_CODE'].count()
print("Percentual de recalls do tipo veículo: %.2f%%"  % (percentual_recalls_veiculos * 100))
Percentual de recalls do tipo veículo: 91.70%
Code
df = df[df['RECALL_TYPE_CODE'].isin(['V', 'I'])]

Modelos de veículos

As colunas VEHICLE_MAKE e VEHICLE_MODEL apresentam, respectivamente, a montadora e modelo de veículos envolvidos no recall. Assim, as colunas foram concatenadas e utilizadas para identificar um veículo no dataset.

Code
df['VEHICLE_MODEL'] = df[['VEHICLE_MAKE', 'VEHICLE_MODEL']].apply(lambda row: ' '.join(row.values.astype(str)), axis=1)
Code
df[['VEHICLE_MODEL', 'VEHICLE_YEAR']].head()
VEHICLE_MODEL VEHICLE_YEAR
0 FORD FOCUS 2000
1 FORD FOCUS 2001
2 JAYCO FT EAGLE 10 SG 2003
3 HOLIDAY RAMBLER ENDEAVOR 2000
4 HOLIDAY RAMBLER ENDEAVOR 1999

Verificamos que alguns recalls possuem marca e modelos que ainda não foram definidos. Esses casos foram removidos do dataset.

Code
df[df['VEHICLE_MODEL'].str.contains('TBD')]
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE
742 02V229000 TBD TBD TBD 9999 EQUIPMENT:OTHER:LABELS V 550.0 Ewr Volpe Acme Inc. 20020822
4103 01V170000 TBD TBD TBD 9999 EQUIPMENT:OTHER:LABELS V 389.0 Ewr Volpe Acme Inc. 20010529
4348 01V111002 TBD TBD TBD 9999 SERVICE BRAKES, AIR:SUPPLY:COMPRESSOR V 667.0 Ewr Volpe Acme Inc. 20010626
Code
df.drop(df[df['VEHICLE_MODEL'].str.contains('TBD')].index, inplace=True)

assert(df[df['VEHICLE_MODEL'].str.contains('TBD')]['VEHICLE_MODEL'].count() == 0)

Podemos verificar por anos de modelo inválidos no dataset. Alguns modelos possuem ano 9999, este valor é utilizado para quando o ano do modelo é desconhecido. Registros com ano de veículo desconhecido foram removidos.

Code
df['VEHICLE_YEAR'].unique()
array([2000, 2001, 2003, 1999, 2002, 1997, 1998, 1996, 2006, 2004, 2005,
       1987, 1988, 1990, 1991, 1993, 1994, 1995, 1989, 1992, 1986, 2010,
       1985, 1982, 1983, 1984, 1978, 1979, 1980, 1981, 1977, 1973, 1976,
       1975, 1974, 1966, 1967, 9999, 1968, 1969, 1971, 1972, 1970, 1965,
       2007, 2008, 1959, 1960, 1964, 1963, 1962, 1961, 1958, 1957, 1956,
       1955, 1950, 1951, 1952, 1953, 1954, 1949, 2009, 2011, 2012, 2013,
       2014, 2021, 2015, 2016, 2017, 2018, 2019, 2020, 2022, 2023, 2024])
Code
UNKNOWN_MODEL_YEAR = 9999

df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR].head(2)
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE
15066 95I003000 MICHELIN MICHELIN 16080R16 9999 TIRES I 1500.0 MICHELIN TIRE CORP. 19951005
15067 95I003000 MICHELIN MICHELIN HI TOUR 79X TL 9999 TIRES I 1500.0 MICHELIN TIRE CORP. 19951005
Code
df.drop(df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR].index, inplace=True)

assert(df[df['VEHICLE_YEAR'] == UNKNOWN_MODEL_YEAR]['VEHICLE_YEAR'].count() == 0)

Data de report do recall

No dataset a data do recall é apresentada como texto, sem formatação específica para ser convertida e categorizada por datas, sendo apresentada no seguinte formato:

Code
df['REPORTED_DATE'].head(5)
0    20021106
1    20021106
2    20020904
3    20020909
4    20020909
Name: REPORTED_DATE, dtype: int64

A fim de facilitar a análise dos dados baseada também nas datas dos recalls, a data em que o recall foi reportado foi convertida para datetime.

Code
REPORT_RECEIVED_DATE_FORMAT = '%Y%m%d'

df['REPORTED_DATE'] = pd.to_datetime(df['REPORTED_DATE'], format=REPORT_RECEIVED_DATE_FORMAT)

df['REPORTED_DATE'].head()
0   2002-11-06
1   2002-11-06
2   2002-09-04
3   2002-09-09
4   2002-09-09
Name: REPORTED_DATE, dtype: datetime64[ns]

Componente

Os componentes possuem nome separado por ‘:’ com informações adicionais sobre o componente. Podemos extrair apenas o conteúdo principal do nome do componente.

Code
df['COMPONENT_NAME'].head(5)
0    ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES
1    ELECTRICAL SYSTEM:12V/24V/48V BATTERY:CABLES
2                          EQUIPMENT:OTHER:LABELS
3                                       STRUCTURE
4                                       STRUCTURE
Name: COMPONENT_NAME, dtype: object
Code
df['COMPONENT_NAME'] = df['COMPONENT_NAME'].str.split(':').str[0]
Code
df['COMPONENT_NAME'].head()
0    ELECTRICAL SYSTEM
1    ELECTRICAL SYSTEM
2            EQUIPMENT
3            STRUCTURE
4            STRUCTURE
Name: COMPONENT_NAME, dtype: object

Alguns componentes possuem um nome diferente para um tipo de componente que já existe no dataset, e podem ser renomeados de forma a melhor agrupar os dados.

Code
DUPLICATED_COMPONENTS_TYPE = {'OTHER': 'UNKNOWN OR OTHER', 'TBD': 'UNKNOWN OR OTHER',
                              'Tether, Lower Anchor (on car seat or vehicle)': 'SEAT BELTS'}

df['COMPONENT_NAME'] = df['COMPONENT_NAME'].replace(DUPLICATED_COMPONENTS_TYPE)

Existe uma grande variedade de componentes para os quais os recalls foram registrados. Como o nível de detalhamento era muito grande, foi optado por agrupá-los em categorias, de forma a ter um conjunto maior de recalls em um nível de detalhamento um pouco menor, facilitando a análise dos dados quando olhando para os grupos de dados.

Code
def get_component_category(component):
    if component in ['SEAT BELTS', 'AIR BAGS', 'FORWARD COLLISION AVOIDANCE', 'VEHICLE SPEED CONTROL', 'ELECTRONIC STABILITY CONTROL', 'LANE DEPARTURE']:
        return 'SECURITY'
    elif component in ['ENGINE', 'ENGINE AND ENGINE COOLING', 'POWER TRAIN', 'TRACTION CONTROL SYSTEM', 'HYBRID PROPULSION SYSTEM']:
        return 'ENGINE'
    elif component in ['SERVICE BRAKES', 'SERVICE BRAKES, ELECTRIC', 'SERVICE BRAKES, AIR', 'SERVICE BRAKES, HYDRAULIC', 'PARKING BRAKE']:
        return 'BRAKE'
    elif component in ['FUEL SYSTEM, DIESEL', 'FUEL SYSTEM, GASOLINE', 'FUEL SYSTEM, OTHER']:
        return 'FUEL'
    elif component in ['ELECTRICAL SYSTEM', 'INTERIOR LIGHTING']:
        return 'ELECTRICAL'
    elif component in ['STRUCTURE', 'SEATS', 'SUSPENSION', 'LATCHES/LOCKS/LINKAGES', 'STEERING', 'WHEELS', 'TIRES']:
        return 'STRUCTURAL'
    elif component in ['TRAILER HITCHES', 'CHILD SEAT', 'EQUIPMENT',  'COMMUNICATION', 'BACK OVER PREVENTION', 'EQUIPMENT ADAPTIVE/MOBILITY']:
        return 'ACCESSORIES'
    elif component in ['VISIBILITY', 'VISIBILITY/WIPER', 'EXTERIOR LIGHTING']:
        return 'VISIBILITY'
    elif component in ['UNKNOWN OR OTHER']:
        return 'OTHER'
    else:
        print('Component without category:', component)
        return component
    
df['COMPONENT_CATEGORY'] = df['COMPONENT_NAME'].apply(get_component_category)
Code
df[['COMPONENT_CATEGORY', 'COMPONENT_NAME']].head(5)
COMPONENT_CATEGORY COMPONENT_NAME
0 ELECTRICAL ELECTRICAL SYSTEM
1 ELECTRICAL ELECTRICAL SYSTEM
2 ACCESSORIES EQUIPMENT
3 STRUCTURAL STRUCTURE
4 STRUCTURAL STRUCTURE

Fabricante

No dataset existem nomes do mesmo fabricante com formas de escritas diferentes (case sensitive). Para evitar duplicações, o nome dos fabricantes foi padronizado em upper case.

Code
df['MANUFACTURER'].head()
0          Ford Motor Company
1          Ford Motor Company
2                 Jayco, Inc.
3    MONACO COACH CORPORATION
4    MONACO COACH CORPORATION
Name: MANUFACTURER, dtype: object
Code
df['MANUFACTURER'] = df['MANUFACTURER'].str.upper()
Code
df['MANUFACTURER'].head()
0          FORD MOTOR COMPANY
1          FORD MOTOR COMPANY
2                 JAYCO, INC.
3    MONACO COACH CORPORATION
4    MONACO COACH CORPORATION
Name: MANUFACTURER, dtype: object

Unidades afetadas pelo recall

Alguns recalls possuem NA no valor de unidades afetadas. Podemos preencher esse valor com a mediana de unidades afetadas por componente de cada fabricante.

Code
df[df['AFFECTED_UNITS'].isna()][['MANUFACTURER', 'COMPONENT_NAME', 'AFFECTED_UNITS']]
MANUFACTURER COMPONENT_NAME AFFECTED_UNITS
593 DAMON MOTOR COACH EQUIPMENT NaN
789 CRANE CARRIER COMPANY VEHICLE SPEED CONTROL NaN
900 GREAT DANE TRAILERS SERVICE BRAKES, AIR NaN
901 GREAT DANE TRAILERS SERVICE BRAKES, AIR NaN
902 GREAT DANE TRAILERS SERVICE BRAKES, AIR NaN
... ... ... ...
94490 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94491 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94492 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94493 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN
94494 FOREST RIVER, INC. ELECTRICAL SYSTEM NaN

220 rows × 3 columns

Code
mediana_unidades_afetadas = df[~df['AFFECTED_UNITS'].isna()][['MANUFACTURER', 'COMPONENT_NAME', 'AFFECTED_UNITS']].groupby(['MANUFACTURER', 'COMPONENT_NAME']).median()
Code
mediana_unidades_afetadas.head()
AFFECTED_UNITS
MANUFACTURER COMPONENT_NAME
4-STAR TRAILERS, INC. EQUIPMENT 110.0
SERVICE BRAKES, HYDRAULIC 227.0
SUSPENSION 4.0
ABC ENGINEERING, CO STEERING 570.0
VISIBILITY 255.0
Code
mediana_unidades_afetadas.loc['4-STAR TRAILERS, INC.', 'EQUIPMENT']['AFFECTED_UNITS']
110.0
Code
def fill_unidades_afetadas(row):
  fabricante = row['MANUFACTURER']
  componente = row['COMPONENT_NAME']
  try:
    row['AFFECTED_UNITS'] = mediana_unidades_afetadas.loc[fabricante, componente]['AFFECTED_UNITS']
  except KeyError:
    pass
  return row

df[df['AFFECTED_UNITS'].isna()] = df[df['AFFECTED_UNITS'].isna()].apply(fill_unidades_afetadas, axis=1)
Code
df[df['AFFECTED_UNITS'].isna()][['RECALL_ID', 'MANUFACTURER', 'COMPONENT_NAME', 'AFFECTED_UNITS']]
RECALL_ID MANUFACTURER COMPONENT_NAME AFFECTED_UNITS
2103 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2104 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2105 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2106 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2107 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2108 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2109 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
2110 02V017000 WESTERN RECREATIONAL VEHICLES, INC. FUEL SYSTEM, OTHER NaN
4211 00V358011 PERFORMANCE FIRST, INC. SUSPENSION NaN
4906 01V097006 MID BUS CORPORATION SEAT BELTS NaN
4907 01V097006 MID BUS CORPORATION SEAT BELTS NaN
10556 98V010001 COACHMEN INDUSTRIES, INC. FUEL SYSTEM, OTHER NaN
10557 98V010001 COACHMEN INDUSTRIES, INC. FUEL SYSTEM, OTHER NaN
10558 98V010001 VIKING HOMES FUEL SYSTEM, OTHER NaN
55377 05V051000 H&W TRAILER EQUIPMENT NaN
73858 09V282000 RIDE-AWAY CORPORATION EQUIPMENT ADAPTIVE/MOBILITY NaN
85033 07V488000 NATIONAL R.V. INC. VEHICLE SPEED CONTROL NaN
86776 09V283000 RIDE-AWAY CORPORATION EQUIPMENT ADAPTIVE/MOBILITY NaN

Resumo da consequência

A coluna CONSEQUENCE_SUMMARY detalha as possíveis consequências do problema relatado para recall. Elas não possuem padronização quanto ao uso de letras maíusculas e minúsculas. Assim, para uma melhor análise, o texto das descrições foi padronizado com o uso de letras minúsculas.

Code
df_consequence = df_raw[['RECALL_ID','CONSEQUENCE_SUMMARY',]].reset_index()
df_consequence['CONSEQUENCE_SUMMARY'] = df_consequence['CONSEQUENCE_SUMMARY'].str.lower()

df_consequence.drop_duplicates(inplace=True)
Code
df_consequence.sample(5)
index RECALL_ID CONSEQUENCE_SUMMARY
129684 129684 18V076000 these deposits may cause a valve within the ab...
221753 221753 22V365000 a disabled ecall system would prevent a vehicl...
193017 193017 22V221000 an overheated alternator bearing can increase ...
6615 6615 00V389000 this condition could result in a loss of vehic...
143024 143024 20V219000 a fire increases the risk of injury. an engin...

Removendo valores NA e duplicações

De forma a trabalhar com dados válidos para as colunas analisadas, foi realizada a remoção de valores NA. Antes da limpeza, a coluna AFFECTED_UNITS apresentava 18 entradas com valores NA, sendo removidos após a limpeza dos dados.

Code
df.isna().sum()
RECALL_ID              0
VEHICLE_MAKE           0
VEHICLE_MODEL          0
VEHICLE_YEAR           0
COMPONENT_NAME         0
RECALL_TYPE_CODE       0
AFFECTED_UNITS        18
MANUFACTURER           0
REPORTED_DATE          0
COMPONENT_CATEGORY     0
dtype: int64
Code
df.dropna(inplace=True)

df.isna().sum()
RECALL_ID             0
VEHICLE_MAKE          0
VEHICLE_MODEL         0
VEHICLE_YEAR          0
COMPONENT_NAME        0
RECALL_TYPE_CODE      0
AFFECTED_UNITS        0
MANUFACTURER          0
REPORTED_DATE         0
COMPONENT_CATEGORY    0
dtype: int64

Em seguida, também foi realizada a remoção de entradas duplicadas, totalizando ao final cerca de 147 mil entradas válidas para análise.

Code
df.drop_duplicates(inplace=True)

df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 147762 entries, 0 to 237790
Data columns (total 10 columns):
 #   Column              Non-Null Count   Dtype         
---  ------              --------------   -----         
 0   RECALL_ID           147762 non-null  object        
 1   VEHICLE_MAKE        147762 non-null  object        
 2   VEHICLE_MODEL       147762 non-null  object        
 3   VEHICLE_YEAR        147762 non-null  int64         
 4   COMPONENT_NAME      147762 non-null  object        
 5   RECALL_TYPE_CODE    147762 non-null  object        
 6   AFFECTED_UNITS      147762 non-null  float64       
 7   MANUFACTURER        147762 non-null  object        
 8   REPORTED_DATE       147762 non-null  datetime64[ns]
 9   COMPONENT_CATEGORY  147762 non-null  object        
dtypes: datetime64[ns](1), float64(1), int64(1), object(7)
memory usage: 12.4+ MB

Validação e download do dataset

Podemos validar o dataset após a limpeza pesquisando pelas recalls no portal da NHTSA.

Code
df[df['RECALL_ID'] == '02V288000']
# https://www.nhtsa.gov/recalls?nhtsaId=02V288000
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE COMPONENT_CATEGORY
0 02V288000 FORD FORD FOCUS 2000 ELECTRICAL SYSTEM V 291854.0 FORD MOTOR COMPANY 2002-11-06 ELECTRICAL
1 02V288000 FORD FORD FOCUS 2001 ELECTRICAL SYSTEM V 291854.0 FORD MOTOR COMPANY 2002-11-06 ELECTRICAL
Code
df[df['RECALL_ID'] == '02V236000']
#https://www.nhtsa.gov/recalls?nhtsaId=02V236000
RECALL_ID VEHICLE_MAKE VEHICLE_MODEL VEHICLE_YEAR COMPONENT_NAME RECALL_TYPE_CODE AFFECTED_UNITS MANUFACTURER REPORTED_DATE COMPONENT_CATEGORY
2 02V236000 JAYCO JAYCO FT EAGLE 10 SG 2003 EQUIPMENT V 86.0 JAYCO, INC. 2002-09-04 ACCESSORIES
Code
df[df['RECALL_ID'] == '02V237000'].groupby(['VEHICLE_MODEL']).count()['RECALL_ID']
#https://www.nhtsa.gov/recalls?nhtsaId=02V237000
VEHICLE_MODEL
HOLIDAY RAMBLER ADMIRAL       4
HOLIDAY RAMBLER ADMIRAL SE    2
HOLIDAY RAMBLER ENDEAVOR      3
HOLIDAY RAMBLER VACATIONER    4
MONACO COACH LAPALMA          5
MONACO COACH MONARCH          3
MONACO COACH MONARCH SE       2
Name: RECALL_ID, dtype: int64
Code
df.to_csv('data/recalls.csv') 
#files.download('recalls.csv')

Análise exploratória

Para a analise exploratória um dashboard foi implementado utilizando o Dash e está disponível em: https://recalls.eda-dash.lat.

O código-fonte se encontra no github: https://github.com/EdmilsonSantana/recalls-dashboard

A partir do dashboard desenvolvido, foi possível explorar principalmente a relação entre o número de recalls e número de veículos afetados por recalls, definindo filtros quanto ao período de tempo, fabricante e modelo para exploração.

Algumas análises e insights obtidos a partir da exploração do dashboard construído são apresentadas a seguir, embora muitas outras possam ser produzidas a partir de exploração similar.

Diversas análises podem ser conduzidas a partir do Dashboard, mas de forma a exemplificar algumas das análises realizadas, são apresentadas as análises realizadas em relação aos recalls registrados para o ano de 2022.

Análise das recalls de 2022

Code
from datetime import datetime

time_range_start = '2022-01-01'
time_range_end = '2022-12-31'

date_format = '%Y-%m-%d'
start_date = datetime.strptime(time_range_start, date_format)
end_date = datetime.strptime(time_range_end, date_format)

df_2022 = df[df['REPORTED_DATE'].between(start_date, end_date)]

No ano de 2022 ocorreram 932 recalls em 4113 modelos de veículos. Mais de 30 milhões de unidades foram afetadas por recalls, um aumento de aproximadamente 7% em relação a 2021.

Dashboard Initial Page

O fabricante com maior número de recalls e maior número de unidades afetadas foi a Ford Motor Company com 67 recalls e 8.7 milhões de unidades afetadas. Este valor representa cerca de 28% do número de unidades afetadas por recalls em 2022.

Aproxidamente 33% dessas unidades correspondem a apenas um único recall, que afetou o componente Powertrain de 30 modelos de veículos da Ford Motor Company. Este recall foi o que mais afetou unidades de veículos em 2022, com aproximadamente 2.9 milhões de unidades.

O diagrama de violino apresentado a seguir possibilita a visualização e exploração da distribuição do número de veículos afetados em cada recall:

Code
reported_recalls_and_affected_units = df_2022[['RECALL_ID', 'REPORTED_DATE', 'AFFECTED_UNITS']].drop_duplicates()

fig = px.violin(
    reported_recalls_and_affected_units,
    y='AFFECTED_UNITS',
    labels={'AFFECTED_UNITS': 'Vehicle units'},
    color_discrete_sequence=px.colors.qualitative.T10,
    title='Number of affected vehicle units distribution'
)

fig.update_layout(yaxis_title=None, xaxis_title=None)

fig.show()

Figure 1: Number of affected vehicle units ditribution

A Mercedes Benz foi a fabricante com mais modelos de veículos que passaram por recall, foram 340 modelos em 2022. Aproximadamente 40% destes modelos, produzidos entre 2017 até 2022, foram afetados por apenas um único recall que ocorreu nos componentes de comunicação e sistema elétrico.

Code
selected_columns = ['RECALL_ID', 'MANUFACTURER', 'REPORTED_DATE',
                    'VEHICLE_MODEL', 'VEHICLE_YEAR', 'AFFECTED_UNITS']
recalls = df_2022[selected_columns].drop_duplicates()

recalls_distribution = recalls.groupby(
    ['RECALL_ID', 'MANUFACTURER', 'AFFECTED_UNITS', 'REPORTED_DATE']).count().reset_index()

fig = px.scatter(
    recalls_distribution,
    y='VEHICLE_MODEL',
    x='AFFECTED_UNITS',
    hover_name='RECALL_ID',
    hover_data=['MANUFACTURER', 'REPORTED_DATE'],
    color_discrete_sequence=px.colors.qualitative.T10,
    labels={'VEHICLE_MODEL': 'Recalled vehicles',
            'AFFECTED_UNITS': 'Vehicle units',
            'MANUFACTURER': 'Manufacturer',
            'REPORTED_DATE': 'Reported Date'},
    title='Recalls distribution by number of affected units and vehicles recalled'
)

fig.show()

Figure 2: Recalls distribution by number of affected units and vehicles recalled

No ano de 2022, em média apenas uma recall foi reportada por veículo. Porém, existem vários outliers. Alguns exemplos:

  1. Blue Bird Vision, modelo de 2023, com 11 recalls reportadas;
  2. IC Bus CE, modelo de 2023, com 10 recalls reportadas;
  3. Tesla Model X, modelo de 2021 e 2022, com 10 recalls reportadas cada.

A mediana do número de unidades de veículos afetados por recalls em 2022 é de 459 unidades, mas existem vários outliers. Agrupando por modelo de veículo, vários modelos ultrapassam 3 milhões de unidades, tais como:

  1. Ford Fusion, modelo de 2014 e 2015;
  2. Tesla Model Y, modelo de 2020, 2021 e 2022.

OBS: não é possível distinguir o número de unidades para cada modelo de veículo individualmente, apenas por recall. Portanto, este valor não representa o número exato de unidades de um determinado modelo de veículo, mas o número de unidades de veículos que foram afetados por recalls que incluem um determinado modelo.

Code
selected_columns = ['RECALL_ID', 'MANUFACTURER',
                    'VEHICLE_MODEL', 'VEHICLE_YEAR', 'AFFECTED_UNITS']
recalls = df_2022[selected_columns].drop_duplicates()

recalls['VEHICLE'] = recalls['VEHICLE_MODEL'] + ' - ' + recalls['VEHICLE_YEAR'].astype(str)

affected_units_by_vehicle = recalls.groupby(['MANUFACTURER', 'VEHICLE']).sum(numeric_only=True)
recalls_by_vehicle = recalls.groupby(['MANUFACTURER', 'VEHICLE']).count()

recalls_by_vehicle.drop(columns=['AFFECTED_UNITS'], inplace=True)

recalls_and_affected_units_by_vehicle = pd.merge(affected_units_by_vehicle, recalls_by_vehicle, left_index=True, right_index=True).reset_index()


fig = make_subplots(rows=2, cols=1)
x = recalls_and_affected_units_by_vehicle['VEHICLE']
manufacturer = recalls_and_affected_units_by_vehicle['MANUFACTURER']
recalls_mean = recalls_and_affected_units_by_vehicle['RECALL_ID'].median()
affected_units_mean = recalls_and_affected_units_by_vehicle['AFFECTED_UNITS'].median()

fig.add_trace(
    go.Scatter(
        line=dict(color=px.colors.qualitative.T10[0]),
        x=x,
        y=recalls_and_affected_units_by_vehicle['RECALL_ID'],
        mode='markers',
        name='Recalls',
        hovertemplate='<b>Recalls</b>: %{y}' +
        '<br><b>Vehicle</b>: %{x}<br>' +
        '<b>Manufacturer</b>: %{text}',
        text=manufacturer,
        showlegend=False
    ),
    row=1,
    col=1
)

fig.add_trace(
    go.Scatter(
        line=dict(color=px.colors.qualitative.T10[1]),
        x=x,
        y=recalls_and_affected_units_by_vehicle['AFFECTED_UNITS'],
        mode='markers',
        name='Vehicle units',
        hovertemplate='<b>Vehicle Units</b>: %{y:.2f}' +
        '<br><b>Vehicle</b>: %{x}<br>' +
        '<b>Manufacturer</b>: %{text}',
        text=manufacturer,
        showlegend=False
    ),
    row=2,
    col=1
)

fig.add_hline(
    y=recalls_mean,
    line_width=3,
    row=1,
    col=1,
    line_dash="dash",
    annotation_text="Recalls median")

fig.add_hline(
    y=affected_units_mean,
    line_width=3,
    row=2,
    col=1,
    line_dash="dash",
    annotation_text="Vehicle units median")

fig.update_layout(
    title_text='Number of recalls and affected vehicle units by vehicle',
)

y_titles = {'x': 'Recalls', 'x2': 'Vehicle units'}
fig.for_each_yaxis(lambda y: y.update(title=y_titles[y.anchor]))

fig.update_xaxes(showticklabels=False, categoryorder="total descending")

fig.show()

Figure 3: Number of recalls and affected units by vehicle

Análisando os componentes que passaram por recalls e comparando o número de recalls com o número de unidades afetadas, observa-se que alguns componentes apesar de possuirem um número de recalls alto, possuem um número baixo de unidades afetadas se comparado com os demais componentes.

As recalls do componente acessório / equipamento correspondem a aproximadamente 21% do número de recalls do ano de 2022, porém o número de unidades afetadas corresponde a 2% do número total de unidades afetadas. O inverso ocorre com o componente Powertrain, com 5% do número de recalls, mas 15% do número total de unidades afetadas.

Isso pode ter ocorrido devido aos recalls de equipamento não serem tão críticos e não demandarem uma urgência, por parte do proprietário, no reparo ou troca do veículo ou por serem recalls que afetam apenas um pequeno conjunto de veículos.

Code
recalls = df_2022[['RECALL_ID', 'COMPONENT_CATEGORY',
                   'COMPONENT_NAME', 'AFFECTED_UNITS']].drop_duplicates()

recalls_by_component = recalls.groupby(['COMPONENT_CATEGORY', 'COMPONENT_NAME']).count().reset_index()

affected_units_by_component = recalls.groupby(
    ['COMPONENT_CATEGORY', 'COMPONENT_NAME']).sum(numeric_only=True).reset_index()

recalls_by_component.rename({'RECALL_ID': 'VALUE'}, axis=1, inplace=True)
recalls_by_component['CHART_TYPE'] = 'A'

affected_units_by_component.rename({'AFFECTED_UNITS': 'VALUE'}, axis=1, inplace=True)
affected_units_by_component['CHART_TYPE'] = 'B'

recalls_and_affected_units_by_component = pd.concat([recalls_by_component, affected_units_by_component])

fig = px.bar(
    recalls_and_affected_units_by_component,
    color_discrete_sequence=px.colors.qualitative.T10,
    x="COMPONENT_NAME",
    y="VALUE",
    color='COMPONENT_CATEGORY',
    facet_row='CHART_TYPE',
    title='Number of recalls and affected vehicle units by component',
    labels={"COMPONENT_CATEGORY": 'Component Category',
            "COMPONENT_NAME": 'Component Name'}
)

fig.for_each_annotation(lambda a: a.update(text=''))

y_titles = {'x2': 'Recalls', 'x': 'Vehicle units'}
fig.for_each_yaxis(lambda y: y.update(title=y_titles[y.anchor]))

fig.update_yaxes(matches=None)
fig.update_layout(hovermode="x")
fig.update_traces(hovertemplate=None)

fig.show()

Figure 4: Number of recalls and affected vehicle units by component

Esses indicadores podem auxiliar na tomada de decisão do fabricante direcionando os esforços na melhoria de qualidade de seus processos internos, afim de reduzir o número de recalls e de unidades de veículos afetados.

Também foi possível a partir da descrição das consequências de cada registro de recall construir uma nuvem de palavras, de forma a medir termos mais presentes nas descrições, conforme apresentado abaixo:

Code
from wordcloud import WordCloud
from wordcloud import ImageColorGenerator
from wordcloud import STOPWORDS

text = " ".join(str(i) for i in df_consequence['CONSEQUENCE_SUMMARY'])
stopwords = set(STOPWORDS)
wordcloud = WordCloud(width=800, height=400, stopwords=stopwords, background_color="white").generate(text)

plt.figure(figsize=(15,10))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

Figure 5: Consequence summary word cloud

Na visualização, termos como risco (risk), colisão (crash) e ferimento (injury), entre outros termos, foram alguns dos mais presentes nas descrições de possíveis consequências.